package ssh

import (
	"bytes"
	"github.com/kataras/golog"
	"golang.org/x/crypto/ssh"
	"io"
	"math"
	"regexp"
	"sdn-executor/utils/erros"
	"time"
)

type Subsystem string

const (
	SubsystemShell   Subsystem = "shell"
	SubsystemNetconf Subsystem = "netconf"
)

const (
	MORE_PROMPT = "-+[ ]*[\\(]*[ ]*[Mm]ore[ ]*[\\d]*[%]*[ ]*[\\)]*[ ]*-+"
)

type Session struct {
	Client     *Client
	subsystem  Subsystem
	sshSession *ssh.Session
	reader     io.Reader
	writer     io.WriteCloser
}

func (s *Session) RcvUntilFuncs(
	funcs []func([]byte) (int, *erros.Error),
	requestTimeout time.Duration,
	oneLine bool,
	matchMoreLine bool,
) ([]byte, int, *erros.Error) {

	buf := make([]byte, 4*1024)
	lastPos := 0
	matchStartPos := -1
	timeout := time.Duration(math.MaxInt64)
	if requestTimeout > 0 {
		timeout = requestTimeout
	}
	timerC := time.After(timeout)
	for {
		golog.Debug(">>>>>>>>>>>enter for")
		select {
		case <-timerC:
			golog.Errorf("echo read timeout. requestTimeout: %d, device: %s", requestTimeout, s.Client.Device)
			return buf[:lastPos], -1, erros.New(erros.CodeProcessTimeout, "command execution timeout")
		default:

			// 读取数据
			n, err := s.reader.Read(buf[lastPos:])
			// 无论是否出错,都接收读取的数据
			lastPos += n
			if err != nil {
				if err == io.EOF {
					// 尝试进行匹配
					idx, e := s.matchBuf(buf, matchStartPos, lastPos, funcs, oneLine)
					if e != nil {
						return buf[:lastPos], -1, e
					} else if idx > -1 {
						return buf[:lastPos], idx, nil
					}

					// 没匹配到
					golog.Errorf("received io.EOF, but no dedicated prompt. device: %s", s.Client.Device)
					return buf[:lastPos], -1, erros.New(erros.CodeIOError, err.Error())
				} else {
					golog.Errorf("echo read failed. device: %s, err: %+v", s.Client.Device, err)
					return buf[:lastPos], -1, erros.New(erros.CodeIOError, err.Error())
				}
			}

			idx, e := s.matchBuf(buf, matchStartPos, lastPos, funcs, oneLine)
			if e != nil {
				return buf[:lastPos], -1, e
			} else if idx > -1 {
				return buf[:lastPos], idx, nil
			} else {
				if matchMoreLine {
					s.matchMoreLine(buf, matchStartPos, lastPos)
				}
			}

			// 扩容，指数增长
			if len(buf)-256 <= lastPos {
				buf1 := make([]byte, len(buf))
				buf = append(buf, buf1...)
			}
		}
	}

}

func (s *Session) matchBuf(
	buf []byte,
	startPos int,
	lastPos int,
	funcs []func([]byte) (int, *erros.Error),
	oneLine bool,
) (int, *erros.Error) {
	// 寻找新的换行符位置
	if oneLine {
		adjStartPos := 0
		if startPos >= 0 {
			adjStartPos = startPos
		}
		foundNewLinePos := bytes.LastIndexByte(buf[adjStartPos:lastPos], '\n')
		if foundNewLinePos != -1 {
			startPos += foundNewLinePos
		}
	}

	matchBuf := buf[startPos+1 : lastPos]
	for idx, f := range funcs {
		pos, err := f(matchBuf)
		if err != nil {
			golog.Errorf("echo prompt match fail. idx: %d, matchBuf: %s, device: %s, err: %+v",
				idx, string(matchBuf), s.Client.Device, err)
			return -1, err
		}

		if pos > -1 {
			// 匹配到
			golog.Infof("match======, pos: %d, device: %s", pos, s.Client.Device)
			return idx, nil
		}
	}

	// 未匹配到
	return -1, nil
}

func (s *Session) matchMoreLine(
	buf []byte,
	startPos int,
	lastPos int,
) *erros.Error {
	regexp, _ := regexp.Compile(MORE_PROMPT)
	if regexp.Match(buf[startPos+1 : lastPos]) {
		_, err := s.Send("\n")
		if err != nil {
			golog.Errorf("send more line command failed. device: %s, err: %+v", s.Client.Device, err)
			return erros.New(erros.CodeIOError, "send more line command failed.")
		}
	}
	return nil
}

func (s *Session) RcvUntilBytes(
	bs [][]byte,
	requestTimeout time.Duration,
	oneLine bool,
	matchMoreLine bool,
) ([]byte, int, *erros.Error) {
	var funcs []func([]byte) (int, *erros.Error)
	for _, b := range bs {
		ib := b
		golog.Infof("ib: %s", string(b))
		funcs = append(funcs, func(buf []byte) (int, *erros.Error) {
			if len(ib)+128 < len(buf) {
				// 不需要全部匹配
				startPos := len(ib) + 128
				return bytes.LastIndex(buf[startPos:], ib), nil
			}
			// 从后往前匹配
			return bytes.LastIndex(buf, ib), nil
		})
	}

	return s.RcvUntilFuncs(funcs, requestTimeout, oneLine, matchMoreLine)
}

func (s *Session) RcvUntilStrings(
	ss []string,
	requestTimeout time.Duration,
	oneLine bool,
	matchMoreLine bool,
) ([]byte, int, *erros.Error) {
	var bs [][]byte
	for _, s := range ss {
		is := s
		bs = append(bs, []byte(is))
	}

	return s.RcvUntilBytes(bs, requestTimeout, oneLine, matchMoreLine)
}

func (s *Session) RcvUntilString(
	str string,
	requestTimeout time.Duration,
	oneLine bool,
	matchMoreLine bool,
) ([]byte, int, *erros.Error) {
	var bs [][]byte
	bs = append(bs, []byte(str))

	return s.RcvUntilBytes(bs, requestTimeout, oneLine, matchMoreLine)
}

func (s *Session) RcvUntilRegexps(
	exprs []string,
	requestTimeout time.Duration,
	oneLine bool,
	matchMoreLine bool,
) ([]byte, int, *erros.Error) {
	var funcs []func([]byte) (int, *erros.Error)
	for _, expr := range exprs {
		is := expr
		re, err := regexp.Compile(is)
		if err != nil {
			golog.Errorf("compile regexp failed. regex expr: %s, device: %s, err: %+v", is, s.Client.Device, err)
			return nil, -1, erros.New(erros.CodeInvalidQueryParam, "regex compile error")
		}
		funcs = append(funcs, func(buf []byte) (int, *erros.Error) {
			match := re.Match(buf)
			if match {
				return 0, nil
			}
			return -1, nil
		})
	}

	return s.RcvUntilFuncs(funcs, requestTimeout, oneLine, matchMoreLine)
}

func (s *Session) Send(request string) (int, *erros.Error) {
	return s.SendBytes([]byte(request))
}

func (s *Session) SendBytes(request []byte) (int, *erros.Error) {
	n, err := s.writer.Write(request)
	if err != nil {
		return n, erros.New(erros.CodeInternalError, err.Error())
	}
	return n, nil
}

func (s *Session) Close() {
	if s != nil && s.sshSession != nil {
		s.sshSession.Close()
	}
}
